import base64
import os
import time
from io import BytesIO

import emoji
import imagehash
import webcolors
from PIL import Image, ImageChops, ImageStat
from appium.webdriver.common.mobileby import MobileBy
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException
from selenium.webdriver import ActionChains
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait

from tests import transl


class BaseElement(object):
    def __init__(self, driver, **kwargs):
        self.driver = driver
        self.by = MobileBy.XPATH
        self.locator = None
        self.xpath = None
        self.accessibility_id = None
        self.translation_id = None
        self.uppercase = None
        self.prefix = ''
        self.suffix = None
        self.id = None
        self.class_name = None
        self.AndroidUIAutomator = None
        self.webview = None

        self.__dict__.update(kwargs)
        self.set_locator()

    def set_locator(self):
        if self.xpath:
            self.locator = self.xpath
        elif self.accessibility_id:
            self.by = MobileBy.ACCESSIBILITY_ID
            self.locator = self.accessibility_id
        elif self.translation_id:
            text = transl[self.translation_id]
            self.locator = '//*[@text="%s"]' % text
            if self.uppercase:
                self.locator = '//*[@text="%s" or @text="%s"]' % (text, text.upper())
            if self.suffix:
                self.locator += self.suffix
        elif self.id:
            self.by = MobileBy.ID
            self.locator = self.id
        elif self.class_name:
            self.by = MobileBy.CLASS_NAME
            self.locator = self.class_name
        elif self.AndroidUIAutomator:
            self.by = MobileBy.ANDROID_UIAUTOMATOR
            self.locator = self.AndroidUIAutomator
        elif self.webview:
            self.locator = '//*[@text="{0}"] | //*[@content-desc="{desc}"]'.format(self.webview, desc=self.webview)
        if self.prefix:
            self.locator = self.prefix + self.locator
        return self

    @property
    def name(self):
        return self.__class__.__name__

    def navigate(self):
        return None

    def find_element(self):
        for _ in range(3):
            try:
                self.driver.info("Find `%s` by `%s`: `%s`" % (self.name, self.by, self.exclude_emoji(self.locator)))
                return self.driver.find_element(self.by, self.locator)
            except NoSuchElementException:
                raise NoSuchElementException(
                    "Device %s: %s by %s: `%s` is not found on the screen" % (
                        self.driver.number, self.name, self.by, self.locator)) from None
            except Exception as exception:
                # if 'Internal Server Error' in str(exception):
                raise exception

    def find_elements(self):
        return self.driver.find_elements(self.by, self.locator)

    def click(self):
        element = self.find_element()
        try:
            element.click()
        except AttributeError:
            raise Exception("Element: %s\n Element type: %s" % (element, type(element)))
        self.driver.info('Tap on found: %s' % self.name)
        return self.navigate()

    def wait_and_click(self, sec=30):
        self.driver.info("Wait for element `%s` for max %ss and click when it is available" % (self.name, sec))
        self.wait_for_visibility_of_element(sec)
        self.click()

    def click_until_presence_of_element(self, desired_element, attempts=4):
        self.driver.info("Click until `%s` by `%s`: `%s` will be presented" % (
            desired_element.name, desired_element.by, desired_element.locator))
        for _ in range(attempts):
            self.find_element().click()
            if desired_element.is_element_displayed(2):
                return
        raise NoSuchElementException("%s element not found" % desired_element.name)

    def double_click(self):
        self.driver.info('Double tap on: %s' % self.name)
        [self.find_element().click() for _ in range(2)]

    def wait_for_element(self, seconds=10):
        try:
            return WebDriverWait(self.driver, seconds) \
                .until(expected_conditions.presence_of_element_located((self.by, self.locator)))
        except TimeoutException:
            raise TimeoutException(
                "Device `%s`: `%s` by` %s`: `%s` is not found on the screen after wait_for_element" % (
                    self.driver.number, self.name, self.by, self.locator)) from None

    def wait_for_elements(self, seconds=10):
        try:
            return WebDriverWait(self.driver, seconds) \
                .until(expected_conditions.presence_of_all_elements_located((self.by, self.locator)))
        except TimeoutException:
            raise TimeoutException(
                "Device %s:  %s by %s:`%s` is not found on the screen after wait_for_elements" % (
                    self.driver.number, self.name, self.by, self.locator)) from None

    def wait_for_visibility_of_element(self, seconds=10, ignored_exceptions=None):
        try:
            return WebDriverWait(self.driver, seconds, ignored_exceptions=ignored_exceptions) \
                .until(expected_conditions.visibility_of_element_located((self.by, self.locator)))
        except TimeoutException:
            raise TimeoutException(
                "Device %s: %s by %s:`%s` is not found on the screen after wait_for_visibility_of_element" % (
                    self.driver.number, self.name, self.by, self.locator)) from None

    def wait_for_invisibility_of_element(self, seconds=10):
        try:
            return WebDriverWait(self.driver, seconds) \
                .until(expected_conditions.invisibility_of_element_located((self.by, self.locator)))
        except TimeoutException:
            raise TimeoutException(
                "Device %s: %s by %s: `%s`  is still visible on the screen after %s seconds after wait_for_invisibility_of_element" % (
                    self.driver.number, self.name, self.by, self.locator, seconds)) from None

    def wait_for_rendering_ended_and_click(self, attempts=3):
        for i in range(attempts):
            try:
                self.wait_for_visibility_of_element(20)
                self.click()
                self.driver.info("Attempt %s is successful clicking %s" % (i, self.locator))
                return
            except StaleElementReferenceException:
                time.sleep(1)
        raise StaleElementReferenceException(
            msg="Device %s: continuous rendering, can't click an element by %s: %s" % (
                self.driver.number, self.by, self.locator))

    def wait_for_element_text(self, text, wait_time=30, message=None):
        if not isinstance(text, str):
            text = str(text)
        self.driver.info("Wait for text element `%s` to be equal to `%s`" % (self.name, text))
        element_text = str()
        counter = 0
        while counter < wait_time:
            try:
                element_text = self.find_element().text.strip()
            except StaleElementReferenceException:
                time.sleep(1)
                element_text = self.find_element().text.strip()
            if element_text == text:
                self.driver.info('Element %s text is equal to %s' % (self.name, text))
                return
            counter += 10
            time.sleep(10)
        self.driver.fail(message if message else "`%s` is not equal to expected `%s` in %s sec" % (
            element_text, text, wait_time))

    def scroll_to_element(self, depth: int = 9, direction='down',
                          down_start_y=0.4, down_end_y=0.05,
                          right_start_x=0.9, right_end_x=0.1):
        self.driver.info('Scrolling %s to %s' % (direction, self.name))
        for _ in range(depth):
            try:
                return self.find_element()
            except NoSuchElementException:
                size = self.driver.get_window_size()
                if direction == 'down':
                    self.driver.swipe(500, size["height"] * down_start_y, 500, size["height"] * down_end_y)
                elif direction == 'up':
                    self.driver.swipe(500, size["height"] * 0.25, 500, size["height"] * 0.8)
                elif direction == 'right':
                    y = size["height"] * down_start_y
                    self.driver.swipe(size["width"] * right_start_x, y, size["width"] * right_end_x, y)
                else:
                    raise ValueError("Unsupported scroll direction: %s" % direction)
        else:
            raise NoSuchElementException(
                "Device %s: %s by %s: `%s` is not found on the screen" % (
                    self.driver.number, self.name, self.by, self.locator)) from None

    def scroll_and_click(self, direction='down'):
        self.scroll_to_element(direction=direction)
        self.click()

    def is_element_displayed(self, sec=5, ignored_exceptions=None):
        try:
            return self.wait_for_visibility_of_element(sec, ignored_exceptions=ignored_exceptions)
        except TimeoutException:
            return False

    def click_if_shown(self, sec=5):
        if self.is_element_displayed(sec=sec):
            self.click()

    def is_element_disappeared(self, sec=20):
        try:
            return self.wait_for_invisibility_of_element(sec)
        except TimeoutException:
            return False

    @property
    def text(self):
        return self.find_element().text

    @property
    def template(self):
        try:
            return self.__template
        except FileNotFoundError:
            raise FileNotFoundError('Please add %s image as template' % self.name)

    @template.setter
    def template(self, value):
        self.__template = Image.open(os.sep.join(__file__.split(os.sep)[:-1]) + '/elements_templates/%s' % value)

    @property
    def image(self):
        return Image.open(BytesIO(base64.b64decode(self.find_element().screenshot_as_base64)))

    def attribute_value(self, value):
        attribute_value = self.find_element().get_attribute(value)
        if attribute_value.lower() == 'true':
            attribute_state = True
        elif attribute_value.lower() == 'false':
            attribute_state = False
        else:
            attribute_state = attribute_value
        return attribute_state

    # Method-helper for renew screenshots in case if changed
    def save_new_screenshot_of_element(self, name: str):
        full_path_to_file = os.sep.join(__file__.split(os.sep)[:-1]) + '/elements_templates/%s' % name
        screen = Image.open(BytesIO(base64.b64decode(self.find_element().screenshot_as_base64)))
        screen.save(full_path_to_file)

    def is_element_differs_from_template(self, file_name: str = '', diff: int = 0):
        if file_name:
            self.template = file_name
        result = False
        difference = ImageChops.difference(self.image, self.template)
        stat = ImageStat.Stat(difference)
        diff_ratio = sum(stat.mean) / (len(stat.mean) * 255)
        self.driver.info('Image differs from template to %s percents' % str(diff_ratio * 100))
        if diff_ratio * 100 > diff:
            result = True
        return result

    def is_element_image_similar_to_template(self, template_path: str = ''):
        image_template = os.sep.join(__file__.split(os.sep)[:-1]) + '/elements_templates/%s' % template_path
        template = imagehash.average_hash(Image.open(image_template))
        element_image = imagehash.average_hash(self.image)
        return not bool(template - element_image)

    def get_element_rgb(self, colors: int = 2):
        palette = self.image.quantize(colors=colors).getpalette()
        percentage = webcolors.rgb_to_rgb_percent((palette[:3]))
        return (float(percentage.red.strip('%')),
                float(percentage.green.strip('%')),
                float(percentage.blue.strip('%')))

    def get_element_coordinates(self):
        element = self.find_element()
        location = element.location
        size = element.size
        return location, size

    def swipe_left_on_element(self):
        self.driver.info("Swiping left on element %s" % self.name)
        location, size = self.get_element_coordinates()
        x, y = location['x'], location['y']
        width, height = size['width'], size['height']
        self.driver.swipe(start_x=x + width * 0.75, start_y=y + height / 2, end_x=x, end_y=y + height / 2)

    def swipe_right_on_element(self, width_percentage=0.9, start_x=0):
        self.driver.info("Swiping right on element %s" % self.name)
        location, size = self.get_element_coordinates()
        x, y = location['x'], location['y']
        width, height = size['width'], size['height']
        self.driver.swipe(start_x=x + start_x, start_y=y + height / 2, end_x=x + width * width_percentage,
                          end_y=y + height / 2)

    def swipe_to_web_element(self, depth=700):
        element = self.find_element()
        location = element.location
        x, y = location['x'], location['y']
        self.driver.swipe(start_x=x, start_y=y, end_x=x, end_y=depth)

    def long_press_element(self, element_to_release_on=None):
        element = self.find_element()
        self.driver.info("Long press on `%s`" % self.name)
        action = ActionChains(self.driver)
        action.click_and_hold(element).perform()
        time.sleep(2)
        if element_to_release_on:
            action.release(element_to_release_on.find_element())
            action.perform()
        else:
            action.release(element)
            action.perform()

    def long_press_without_release(self):
        action = ActionChains(self.driver)
        action.click_and_hold(self.find_element()).perform()

    def long_press_until_element_is_shown(self, expected_element):
        element = self.find_element()
        self.driver.info("Long press on `%s` until expected element is shown" % self.name)
        action = ActionChains(self.driver)
        for _ in range(3):
            action.click_and_hold(element).perform()
            time.sleep(2)
            action.release(element).perform()
            if expected_element.is_element_displayed():
                return

    def long_press_element_by_coordinate(self, rel_x=0.8, rel_y=0.8):
        element = self.find_element()
        location = element.location
        size = element.size
        x = int(location['x'] + size['width'] * rel_x)
        y = int(location['y'] + size['height'] * rel_y)
        action = ActionChains(self.driver)
        action.move_to_element_with_offset(to_element=element, xoffset=x, yoffset=y).click_and_hold().perform()
        action.release(element).perform()

    def click_inside_element_by_coordinate(self, rel_x=0.8, rel_y=0.8, times_to_click=1):
        location, size = self.get_element_coordinates()
        x = int(location['x'] + size['width'] * rel_x)
        y = int(location['y'] + size['height'] * rel_y)
        [self.driver.tap([(x, y)], 150) for _ in range(times_to_click)]

    @staticmethod
    def get_translation_by_key(key):
        return transl[key]

    @staticmethod
    def exclude_emoji(value):
        return 'emoji' if value in emoji.UNICODE_EMOJI else value

    def get_child_element_by_text(self, text: str):
        return BaseElement(self.driver, prefix=self.locator, xpath="//*[@text='%s']" % text)

    def get_child_element_by_text_part(self, text: str):
        return BaseElement(self.driver, prefix=self.locator, xpath="//*[contains(@text,'%s')]" % text)


class EditBox(BaseElement):

    def __init__(self, driver, **kwargs):
        super(EditBox, self).__init__(driver, **kwargs)

    def send_keys(self, value):
        self.find_element().send_keys(value)
        self.driver.info("Type `%s` to `%s`" % (self.exclude_emoji(value), self.name))

    def clear(self):
        self.find_element().clear()
        self.driver.info("Clear text in `%s`" % self.name)

    def delete_last_symbols(self, number_of_symbols_to_delete: int):
        self.driver.info("Delete last `%s` symbols from `%s`" % (number_of_symbols_to_delete, self.name))
        self.click()
        for _ in range(number_of_symbols_to_delete):
            time.sleep(1)
            self.driver.press_keycode(67)

    def paste_text_from_clipboard(self):
        self.driver.info("Paste text from clipboard into `%s`" % self.name)
        self.long_press_element()
        time.sleep(2)
        action = ActionChains(self.driver)
        element = self.find_element()
        location = element.location
        x, y = location['x'], location['y']
        action.move_by_offset(xoffset=x + 25, yoffset=y - 50).click().perform()
        action.release(element).perform()

    def cut_text(self):
        self.driver.info("Cut text in `%s`" % self.name)
        element = self.find_element()
        location = element.location
        x, y = location['x'], location['y']
        action = ActionChains(self.driver)
        action.move_by_offset(xoffset=x, yoffset=y).click_and_hold().perform()
        action.release(element).perform()
        time.sleep(2)
        action.move_by_offset(xoffset=x + 50, yoffset=y - 50).click().perform()
        action.release(element).perform()


class Text(BaseElement):
    def __init__(self, driver, **kwargs):
        super(Text, self).__init__(driver, **kwargs)

    @property
    def text(self):
        text = self.find_element().text
        self.driver.info("`%s` is `%s`" % (self.name, text))
        return text


class Button(BaseElement):

    def __init__(self, driver, **kwargs):
        super(Button, self).__init__(driver, **kwargs)

    def click_until_absense_of_element(self, desired_element, attempts=3, timeout=1):
        counter = 0
        self.driver.info("Click until `%s` by `%s`: `%s` is NOT presented" % (
            desired_element.name, desired_element.by, desired_element.locator))
        while desired_element.is_element_displayed(timeout) and counter <= attempts:
            try:
                self.find_element().click()
                counter += 1
            except (NoSuchElementException, TimeoutException, StaleElementReferenceException):
                return self.navigate()


class SilentButton(Button):
    def find_element(self):
        for _ in range(3):
            try:
                return self.driver.find_element(self.by, self.locator)
            except NoSuchElementException:
                raise NoSuchElementException(
                    "Device %s: `%s` by `%s`:`%s` not found on the screen" % (
                        self.driver.number, self.name, self.by, self.locator)) from None
            except Exception as exception:
                if 'Internal Server Error' in str(exception):
                    continue

    def click(self):
        self.find_element().click()
        return self.navigate()

    @property
    def text(self):
        text = self.find_element().text
        return text


class CheckBox(Button):
    def __init__(self, driver, **kwargs):
        super(Button, self).__init__(driver, **kwargs)

    def __define_desired_element(self, elem_accessibility):
        desired_element_accessibility_id = elem_accessibility
        if self.accessibility_id is not None and ':' in self.accessibility_id:
            desired_element_accessibility_id = ':%s' % elem_accessibility
        return desired_element_accessibility_id

    def enable(self):
        self.click_until_presence_of_element(Button(self.driver,
                                                    accessibility_id=self.__define_desired_element("checkbox-on")))
        return self.navigate()

    def disable(self):
        self.click_until_presence_of_element(Button(self.driver,
                                                    accessibility_id=self.__define_desired_element("checkbox-off")))
        return self.navigate()
